/*
 * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
 * Copyright 2004 The Apache Software Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.glassfish.grizzly.http.util;

import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.Charset;

/**
 * This class is used to represent a subarray of bytes in an HTTP message. It represents all request/response elements.
 * The byte/char conversions are delayed and cached. Everything is recyclable.
 *
 * The object can represent a byte[], a char[], or a (sub) String. All operations can be made in case sensitive mode or
 * not.
 *
 * @author dac@eng.sun.com
 * @author James Todd [gonzo@eng.sun.com]
 * @author Costin Manolache
 */
public final class MessageBytes implements Cloneable, Serializable {
    // primary type ( whatever is set as original value )
    private int type = T_NULL;

    public static final int T_NULL = 0;
    /**
     * getType() is T_STR if the the object used to create the MessageBytes was a String
     */
    public static final int T_STR = 1;
    /**
     * getType() is T_STR if the the object used to create the MessageBytes was a byte[]
     */
    public static final int T_BYTES = 2;
    /**
     * getType() is T_STR if the the object used to create the MessageBytes was a char[]
     */
    public static final int T_CHARS = 3;

    private int hashCode = 0;
    // did we computed the hashcode ?
    private boolean hasHashCode = false;

    // Is the represented object case sensitive ?
    private boolean caseSensitive = true;

    // Internal objects to represent array + offset, and specific methods
    private final ByteChunk byteC = new ByteChunk();
    private final CharChunk charC = new CharChunk();

    // String
    private String strValue;
    // true if a String value was computed. Probably not needed,
    // strValue!=null is the same
    private boolean hasStrValue = false;

    /**
     * Creates a new, uninitialized MessageBytes object.
     * 
     * @deprecated Use static newInstance() in order to allow future hooks.
     */
    @Deprecated
    public MessageBytes() {
    }

    /**
     * Construct a new MessageBytes instance
     */
    public static MessageBytes newInstance() {
        return factory.newInstance();
    }

    /**
     * Configure the case sensitivity
     */
    public void setCaseSenitive(boolean b) {
        caseSensitive = b;
    }

    public MessageBytes getClone() {
        try {
            return (MessageBytes) this.clone();
        } catch (Exception ex) {
            return null;
        }
    }

    public boolean isNull() {
//		should we check also hasStrValue ???
        return byteC.isNull() && charC.isNull() && !hasStrValue;
        // bytes==null && strValue==null;
    }

    /**
     * Resets the message bytes to an uninitialized (NULL) state.
     */
    public void recycle() {
        type = T_NULL;
        byteC.recycle();
        charC.recycle();

        strValue = null;
        caseSensitive = true;

        hasStrValue = false;
        hasHashCode = false;
        hasIntValue = false;
        hasLongValue = false;
    }

    /**
     * Sets the content to the specified subarray of bytes.
     *
     * @param b the bytes
     * @param off the start offset of the bytes
     * @param len the length of the bytes
     */
    public void setBytes(byte[] b, int off, int len) {
        byteC.setBytes(b, off, len);
        type = T_BYTES;
        hasStrValue = false;
        hasHashCode = false;
        hasIntValue = false;
        hasLongValue = false;
    }

    /**
     * Set the encoding. If the object was constructed from bytes[]. any previous conversion is reset. If no encoding is
     * set, we'll use 8859-1.
     */
    public void setCharset(Charset enc) {
        if (!byteC.isNull()) {
            // if the encoding changes we need to reset the converion results
            charC.recycle();
            hasStrValue = false;
        }
        byteC.setCharset(enc);
    }

    /**
     * Sets the content to be a char[]
     *
     * @param c the bytes
     * @param off the start offset of the bytes
     * @param len the length of the bytes
     */
    public void setChars(char[] c, int off, int len) {
        charC.setChars(c, off, len);
        type = T_CHARS;
        hasStrValue = false;
        hasHashCode = false;
        hasIntValue = false;
        hasLongValue = false;
    }

    /**
     * Remove the cached string value. Use it after a conversion on the byte[] or after the encoding is changed XXX Is this
     * needed ?
     */
    public void resetStringValue() {
        if (type != T_STR) {
            // If this was cread as a byte[] or char[], we remove
            // the old string value
            hasStrValue = false;
            strValue = null;
        }
    }

    /**
     * Set the content to be a string
     */
    public void setString(String s) {
        strValue = s;
        hasHashCode = false;
        hasIntValue = false;
        hasLongValue = false;
        if (s == null) {
            hasStrValue = false;
            type = T_NULL;
        } else {
            hasStrValue = true;
            type = T_STR;
        }
    }

    // -------------------- Conversion and getters --------------------

    /**
     * Compute the string value
     */
    @Override
    public String toString() {
        if (hasStrValue) {
            return strValue;
        }
        hasStrValue = true;

        switch (type) {
        case T_CHARS:
            strValue = charC.toString();
            return strValue;
        case T_BYTES:
            strValue = byteC.toString();
            return strValue;
        }
        return "";
    }

    // ----------------------------------------
    /**
     * Return the type of the original content. Can be T_STR, T_BYTES, T_CHARS or T_NULL
     */
    public int getType() {
        return type;
    }

    /**
     * Returns the byte chunk, representing the byte[] and offset/length. Valid only if T_BYTES or after a conversion was
     * made.
     */
    public ByteChunk getByteChunk() {
        return byteC;
    }

    /**
     * Returns the char chunk, representing the char[] and offset/length. Valid only if T_CHARS or after a conversion was
     * made.
     */
    public CharChunk getCharChunk() {
        return charC;
    }

    /**
     * Returns the string value. Valid only if T_STR or after a conversion was made.
     */
    public String getString() {
        return strValue;
    }

    /**
     * Unimplemented yet. Do a char->byte conversion.
     */
    public void toBytes() {
        if (!byteC.isNull()) {
            type = T_BYTES;
            return;
        }
        toString();
        type = T_BYTES;
        byte bb[] = strValue.getBytes(byteC.getCharset());
        byteC.setBytes(bb, 0, bb.length);
    }

    /**
     * Convert to char[] and fill the CharChunk. XXX Not optimized - it converts to String first.
     */
    public void toChars() {
        if (!charC.isNull()) {
            type = T_CHARS;
            return;
        }
        // inefficient
        toString();
        type = T_CHARS;
        char cc[] = strValue.toCharArray();
        charC.setChars(cc, 0, cc.length);
    }

    /**
     * Returns the length of the original buffer. Note that the length in bytes may be different from the length in chars.
     */
    public int getLength() {
        if (type == T_BYTES) {
            return byteC.getLength();
        }
        if (type == T_CHARS) {
            return charC.getLength();
        }
        if (type == T_STR) {
            return strValue.length();
        }
        toString();
        if (strValue == null) {
            return 0;
        }
        return strValue.length();
    }

    // -------------------- equals --------------------

    /**
     * Compares the message bytes to the specified String object.
     * 
     * @param s the String to compare
     * @return true if the comparison succeeded, false otherwise
     */
    public boolean equals(String s) {
        if (s == null) {
            return false;
        }

        if (!caseSensitive) {
            return equalsIgnoreCase(s);
        }
        switch (type) {
        case T_STR:
            return strValue != null && strValue.equals(s);
        case T_CHARS:
            return charC.equals(s);
        case T_BYTES:
            return byteC.equals(s);
        default:
            return false;
        }
    }

    /**
     * Compares the message bytes to the specified String object.
     * 
     * @param s the String to compare
     * @return true if the comparison succeeded, false otherwise
     */
    public boolean equalsIgnoreCase(String s) {
        if (s == null) {
            return false;
        }
        switch (type) {
        case T_STR:
            return strValue != null && strValue.equalsIgnoreCase(s);
        case T_CHARS:
            return charC.equalsIgnoreCase(s);
        case T_BYTES:
            return byteC.equalsIgnoreCase(s);
        default:
            return false;
        }
    }

    @Override
    public boolean equals(Object obj) {

        if (obj == null || !(obj instanceof MessageBytes)) {
            return false;
        }

        MessageBytes mb = (MessageBytes) obj;

        switch (type) {
        case T_STR:
            return mb.equals(strValue);
        }

        if (mb.type != T_CHARS && mb.type != T_BYTES) {
            // it's a string or int/date string value
            return equals(mb.toString());
        }

        // mb is either CHARS or BYTES.
        // this is either CHARS or BYTES
        // Deal with the 4 cases ( in fact 3, one is simetric)

        if (mb.type == T_CHARS && type == T_CHARS) {
            return charC.equals(mb.charC);
        }
        if (mb.type == T_BYTES && type == T_BYTES) {
            return byteC.equals(mb.byteC);
        }
        if (mb.type == T_CHARS && type == T_BYTES) {
            return byteC.equals(mb.charC);
        }
        if (mb.type == T_BYTES && type == T_CHARS) {
            return mb.byteC.equals(charC);
        }
        // can't happen
        return true;
    }

    /**
     * Returns true if the message bytes starts with the specified string.
     * 
     * @param s the string
     */
    public boolean startsWith(String s) {
        switch (type) {
        case T_STR:
            return strValue.startsWith(s);
        case T_CHARS:
            return charC.startsWith(s);
        case T_BYTES:
            return byteC.startsWith(s);
        default:
            return false;
        }
    }

    /**
     * Returns true if the message bytes starts with the specified string.
     * 
     * @param s the string
     * @param pos The start position
     */
    public boolean startsWithIgnoreCase(String s, int pos) {
        switch (type) {
        case T_STR:
            if (strValue == null) {
                return false;
            }
            if (strValue.length() < pos + s.length()) {
                return false;
            }

            for (int i = 0; i < s.length(); i++) {
                if (Ascii.toLower(s.charAt(i)) != Ascii.toLower(strValue.charAt(pos + i))) {
                    return false;
                }
            }
            return true;
        case T_CHARS:
            return charC.startsWithIgnoreCase(s, pos);
        case T_BYTES:
            return byteC.startsWithIgnoreCase(s, pos);
        default:
            return false;
        }
    }

    // -------------------- Hash code --------------------
    @Override
    public int hashCode() {
        if (hasHashCode) {
            return hashCode;
        }
        int code;

        if (caseSensitive) {
            code = hash();
        } else {
            code = hashIgnoreCase();
        }
        hashCode = code;
        hasHashCode = true;
        return code;
    }

    // normal hash.
    private int hash() {
        int code = 0;
        switch (type) {
        case T_STR:
            // We need to use the same hash function
            for (int i = 0; i < strValue.length(); i++) {
                code = code * 37 + strValue.charAt(i);
            }
            return code;
        case T_CHARS:
            return charC.hash();
        case T_BYTES:
            return byteC.hash();
        default:
            return 0;
        }
    }

    // hash ignoring case
    private int hashIgnoreCase() {
        int code = 0;
        switch (type) {
        case T_STR:
            for (int i = 0; i < strValue.length(); i++) {
                code = code * 37 + Ascii.toLower(strValue.charAt(i));
            }
            return code;
        case T_CHARS:
            return charC.hashIgnoreCase();
        case T_BYTES:
            return byteC.hashIgnoreCase();
        default:
            return 0;
        }
    }

    public int indexOf(char c) {
        return indexOf(c, 0);
    }

    // Inefficient initial implementation. Will be replaced on the next
    // round of tune-up
    public int indexOf(String s, int starting) {
        toString();
        return strValue.indexOf(s, starting);
    }

    // Inefficient initial implementation. Will be replaced on the next
    // round of tune-up
    public int indexOf(String s) {
        return indexOf(s, 0);
    }

    public int indexOfIgnoreCase(String s, int starting) {
        toString();
        String upper = strValue.toUpperCase();
        String sU = s.toUpperCase();
        return upper.indexOf(sU, starting);
    }

    /**
     * Returns true if the message bytes starts with the specified string.
     * 
     * @param c the character
     * @param starting The start position
     */
    public int indexOf(char c, int starting) {
        switch (type) {
        case T_STR:
            return strValue.indexOf(c, starting);
        case T_CHARS:
            return charC.indexOf(c, starting);
        case T_BYTES:
            return byteC.indexOf(c, starting);
        default:
            return -1;
        }
    }

    /**
     * Copy the src into this MessageBytes, allocating more space if needed
     */
    public void duplicate(MessageBytes src) throws IOException {
        // START CR 6309514
        // Discard previous state before duplicating
        recycle();
        // END CR 6309514

        switch (src.getType()) {
        case MessageBytes.T_BYTES:
            type = T_BYTES;
            ByteChunk bc = src.getByteChunk();
            byteC.allocate(2 * bc.getLength(), -1);
            byteC.append(bc);
            break;
        case MessageBytes.T_CHARS:
            type = T_CHARS;
            CharChunk cc = src.getCharChunk();
            charC.allocate(2 * cc.getLength(), -1);
            charC.append(cc);
            break;
        case MessageBytes.T_STR:
            type = T_STR;
            String sc = src.getString();
            this.setString(sc);
            break;
        }
    }

    // -------------------- Deprecated code --------------------
    // efficient int, long and date
    // XXX used only for headers - shouldn't be
    // stored here.
    private int intValue;
    private boolean hasIntValue = false;
    private long longValue;
    private boolean hasLongValue = false;

    /**
     * Set the buffer to the representation of an int
     */
    public void setInt(int i) {
        byteC.allocate(16, 32);
        int current = i;
        byte[] buf = byteC.getBuffer();
        int start = 0;
        int end = 0;
        if (i == 0) {
            buf[end++] = (byte) '0';
        }
        if (i < 0) {
            current = -i;
            buf[end++] = (byte) '-';
        }
        while (current > 0) {
            int digit = current % 10;
            current = current / 10;
            buf[end++] = HexUtils.HEX[digit];
        }
        byteC.setStart(0);
        byteC.setEnd(end);
        // Inverting buffer
        end--;
        if (i < 0) {
            start++;
        }
        while (end > start) {
            byte temp = buf[start];
            buf[start] = buf[end];
            buf[end] = temp;
            start++;
            end--;
        }
        intValue = i;
        hasStrValue = false;
        hasHashCode = false;
        hasIntValue = true;
        hasLongValue = false;
        type = T_BYTES;
    }

    /**
     * Set the buffer to the representation of an long
     */
    public void setLong(long l) {
        byteC.allocate(32, 64);
        long current = l;
        byte[] buf = byteC.getBuffer();
        int start = 0;
        int end = 0;
        if (l == 0) {
            buf[end++] = (byte) '0';
        }
        if (l < 0) {
            current = -l;
            buf[end++] = (byte) '-';
        }
        while (current > 0) {
            int digit = (int) (current % 10);
            current = current / 10;
            buf[end++] = HexUtils.HEX[digit];
        }
        byteC.setStart(0);
        byteC.setEnd(end);
        // Inverting buffer
        end--;
        if (l < 0) {
            start++;
        }
        while (end > start) {
            byte temp = buf[start];
            buf[start] = buf[end];
            buf[end] = temp;
            start++;
            end--;
        }
        longValue = l;
        hasStrValue = false;
        hasHashCode = false;
        hasIntValue = false;
        hasLongValue = true;
        type = T_BYTES;
    }

    // Used for headers conversion
    /**
     * Convert the buffer to an int, cache the value
     */
    public int getInt() {
        if (hasIntValue) {
            return intValue;
        }

        switch (type) {
        case T_BYTES:
            intValue = byteC.getInt();
            break;
        default:
            intValue = Integer.parseInt(toString());
        }
        hasIntValue = true;
        return intValue;
    }

    // Used for headers conversion
    /**
     * Convert the buffer to an long, cache the value
     */
    public long getLong() {
        if (hasLongValue) {
            return longValue;
        }

        switch (type) {
        case T_BYTES:
            longValue = byteC.getLong();
            break;
        default:
            longValue = Long.parseLong(toString());
        }

        hasLongValue = true;
        return longValue;

    }

    // -------------------- Future may be different --------------------

    private static MessageBytesFactory factory = new MessageBytesFactory();

    public static void setFactory(MessageBytesFactory mbf) {
        factory = mbf;
    }

    public static class MessageBytesFactory {
        protected MessageBytesFactory() {
        }

        @SuppressWarnings({ "deprecation" })
        public MessageBytes newInstance() {
            return new MessageBytes();
        }
    }
}
